//
// OpenTemplate
// Copyright (c) 2006
// by OpenArrow Software ( http://www.openarrow.com )
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 
// documentation files (the "Software"), to deal in the Software without restriction, including without limitation 
// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 
// to permit persons to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial portions 
// of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 
// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 
// CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
// DEALINGS IN THE SOFTWARE.
//

using System;
using System.Collections.Generic;
using System.Text;
using OpenArrow.Reporting;
using System.Collections.Specialized;
using System.IO;
using OpenArrow.Templating.Parsing;
using System.CodeDom;
using OpenArrow.Templating.Directives;
using OpenArrow.Templating.Compilation;
using System.CodeDom.Compiler;
using OpenArrow.Templating.Properties;

namespace OpenArrow.Templating
{
    /// <summary>
    /// Utility class that process a template through all three phases.
    /// </summary>
    /// <remarks>
    /// <para>
    /// Template Processing occurs in three phases: Parsing, Generation, and Compilation. In the Parsing phase,
    /// an implementation of <see cref="OpenArrow.Templating.Parsing.ITemplateParser"/> parses the template file
    /// into a <see cref="OpenArrow.Templating.Parsing.ParsedTemplate"/>. In the Generation phase, an implementation
    /// of <see cref="OpenArrow.Templating.Compilation.ITemplateGenerator"/> generates a <see cref="System.CodeDom.CodeCompileUnit"/>
    /// from the <see cref="OpenArrow.Templating.Parsing.ParsedTemplate"/>. Finally, in the Compilation phase, an implementation
    /// of <see cref="System.CodeDom.Compiler.CodeDomProvider"/> compiles an Assembly out of the <see cref="System.CodeDom.CodeCompileUnit"/>.
    /// </para>
    /// <para>
    /// Any errors that occur throughout the process are added to a <see cref="ProblemCollection"/> that is either returned within the
    /// <see cref="ProcessorResults"/> object returned by <see cref="TemplateProcessor.ProcessFiles"/> or in an
    /// <see cref="ActionProblemsException"/> if the errors stopped processing from occuring.
    /// </para>
    /// <example language="C#">
    /// <code>
    /// using OpenArrow.Templating;
    ///  
    /// public class MyQuickGenerator() 
    /// {
    /// 	public static void Main(string[] args)
    /// 	{
    ///         ProcessorParameters params = new ProcessorParameters();
    ///         params.CompilerParameters.OutputAssembly = "MyTemplates.dll";
    ///         ProcessorResults results = TemplateProcessor.ProcessFiles(new string[] { "MyTemplate1.otx", "Template2.template",
    ///             "UtilityCode.cs", "Bootstrapper.cs" }, params);
    ///         
    ///         if(results.Success)
    ///             Console.WriteLine("Templates successfully processed!");
    ///         else
    ///             Console.WriteLine("Template processing failed!");
    ///     }
    /// }
    /// </code>
    /// </example>
    /// <example language="VB">Coming Soon!</example>
    /// <example language="C++">Sorry, no C++ examples are available. Use the C# example as a basis.</example>
    /// </remarks>
    public static class TemplateProcessor
    {
        #region Private Fields

        // Not my usual style, prefixing with '_', but it avoids confusion 
        // in the ProcessFiles methods (two of them have parameters named: generator and parser)
        private static ITemplateGenerator _generator;
        private static ITemplateParser _parser; 

        #endregion

        #region Public Properties

        /// <summary>
        /// Gets or sets the <see cref="OpenArrow.Templating.Compilation.ITemplateGenerator"/> implementation
        /// to use during the Generation phase of template processing
        /// </summary>
        public static ITemplateGenerator Generator
        {
            get
            {
                if (_generator == null)
                    _generator = new StandardTemplateGenerator();
                return _generator;
            }
            set { TemplateProcessor._generator = value; }
        }

        /// <summary>
        /// Gets or sets the <see cref="OpenArrow.Templating.Parsing.ITemplateParser"/> implementation
        /// to use during the Parsing phase of template processing
        /// </summary>
        public static ITemplateParser Parser
        {
            get
            {
                if (_parser == null)
                    _parser = new AspNetStyleTemplateParser();
                return _parser;
            }
            set { TemplateProcessor._parser = value; }
        } 

        #endregion

        #region Public Methods

        /// <summary>
        /// Processes the template and source files specified and produces the result
        /// </summary>
        /// <remarks>
        /// This method uses the parser and generator specified in <see cref="TemplateProcessor.Parser"/> 
        /// and <see cref="TemplateProcessor.Generator"/> respectively
        /// </remarks>
        /// <param name="files">The files to process</param>
        /// <param name="parameters">The parameters to pass to the various processing phases (parser, generator, compiler)</param>
        /// <returns>The results of processing</returns>
        /// <exception cref="OpenArrow.Templating.ActionProblemsException">A large number of errors occurred in the operation and it had to be aborted</exception>
        /// <exception cref="System.IO.IOException">An I/O error occurs</exception>
        /// <exception cref="ArgumentNullException"><paramref name="files"/> is <see langword="null"/></exception>
        /// <exception cref="ArgumentNullException"><paramref name="parameters"/> is <see langword="null"/></exception>
        public static ProcessorResults ProcessFiles(string[] files, ProcessorParameters parameters)
        {
            return ProcessFiles(files, parameters, new NullProgressReporter());
        }

        /// <summary>
        /// Processes the template and source files specified and produces the result
        /// </summary>
        /// <remarks>
        /// <para>This method reports its progress to a given progress reporter</para>
        /// <para>
        /// This method uses the parser and generator specified in <see cref="TemplateProcessor.Parser"/> 
        /// and <see cref="TemplateProcessor.Generator"/> respectively
        /// </para>
        /// </remarks>
        /// <param name="files">The files to process</param>
        /// <param name="parameters">The parameters to pass to the various processing phases (parser, generator, compiler)</param>
        /// <param name="reporter">The progress reporter to report to</param>
        /// <returns>The results of processing</returns>
        /// <exception cref="OpenArrow.Templating.ActionProblemsException">A large number of errors occurred in the operation and it had to be aborted</exception>
        /// <exception cref="System.IO.IOException">An I/O error occurs</exception>
        /// <exception cref="ArgumentNullException"><paramref name="files"/> is <see langword="null"/></exception>
        /// <exception cref="ArgumentNullException"><paramref name="parameters"/> is <see langword="null"/></exception>
        public static ProcessorResults ProcessFiles(string[] files, ProcessorParameters parameters, IProgressReporter reporter)
        {
            return ProcessFiles(files, parameters, TemplateProcessor.Generator, TemplateProcessor.Parser, reporter);
        }

        /// <summary>
        /// Processes the template and source files specified and produces the result
        /// </summary>
        /// <remarks>This method reports its progress to a given progress reporter</remarks>
        /// <param name="files">The files to process</param>
        /// <param name="parameters">The parameters to pass to the various processing phases (parser, generator, compiler)</param>
        /// <param name="generator">The <see cref="ITemplateGenerator"/> implementation to use to generate source code</param>
        /// <param name="parser">The <see cref="ITemplateParser"/> implementation to use to parse template files</param>
        /// <returns>The results of processing</returns>
        /// <exception cref="OpenArrow.Templating.ActionProblemsException">A large number of errors occurred in the operation and it had to be aborted</exception>
        /// <exception cref="System.IO.IOException">An I/O error occurs</exception>
        /// <exception cref="ArgumentNullException"><paramref name="files"/> is <see langword="null"/></exception>
        /// <exception cref="ArgumentNullException"><paramref name="parameters"/> is <see langword="null"/></exception>
        /// <exception cref="ArgumentNullException"><paramref name="generator"/> is <see langword="null"/></exception>
        /// <exception cref="ArgumentNullException"><paramref name="parser"/> is <see langword="null"/></exception>
        public static ProcessorResults ProcessFiles(string[] files, ProcessorParameters parameters, ITemplateGenerator generator, ITemplateParser parser)
        {
            return ProcessFiles(files, parameters, generator, parser, new NullProgressReporter());
        }

        /// <summary>
        /// Processes the template and source files specified and produces the result
        /// </summary>
        /// <remarks>This method reports its progress to a given progress reporter</remarks>
        /// <param name="files">The files to process</param>
        /// <param name="parameters">The parameters to pass to the various processing phases (parser, generator, compiler)</param>
        /// <param name="reporter">The progress reporter to report to</param>
        /// <param name="generator">The <see cref="ITemplateGenerator"/> implementation to use to generate source code</param>
        /// <param name="parser">The <see cref="ITemplateParser"/> implementation to use to parse template files</param>
        /// <returns>The results of processing</returns>
        /// <exception cref="OpenArrow.Templating.ActionProblemsException">A large number of errors occurred in the operation and it had to be aborted</exception>
        /// <exception cref="System.IO.IOException">An I/O error occurs</exception>
        /// <exception cref="ArgumentNullException"><paramref name="files"/> is <see langword="null"/></exception>
        /// <exception cref="ArgumentNullException"><paramref name="parameters"/> is <see langword="null"/></exception>
        /// <exception cref="ArgumentNullException"><paramref name="generator"/> is <see langword="null"/></exception>
        /// <exception cref="ArgumentNullException"><paramref name="parser"/> is <see langword="null"/></exception>
        public static ProcessorResults ProcessFiles(string[] files, ProcessorParameters parameters, ITemplateGenerator generator, ITemplateParser parser, IProgressReporter reporter)
        {
            if (files == null)
                throw new ArgumentNullException(String.Format(Resources.Exception_ArgIsNull, "files"), "files");
            if (parameters == null)
                throw new ArgumentNullException(String.Format(Resources.Exception_ArgIsNull, "parameters"), "parameters");
            if (generator == null)
                throw new ArgumentNullException(String.Format(Resources.Exception_ArgIsNull, "generator"), "generator");
            if (parser == null)
                throw new ArgumentNullException(String.Format(Resources.Exception_ArgIsNull, "parser"), "parser");
            if (reporter == null)
                reporter = new NullProgressReporter();

            // Setup Results
            ProcessorResults results = new ProcessorResults();

            // Split files into templates and code files
            List<FileInfo> templateFiles = new List<FileInfo>();
            List<FileInfo> codeFiles = new List<FileInfo>();
            if (!SplitFilesIntoTemplatesAndCodeFiles(files, parameters, reporter, results, templateFiles, codeFiles))
                return results;

            // Parse the templates and compile into source
            List<ReferenceDirective> references = new List<ReferenceDirective>();
            List<string> compilerInputs = new List<string>();
            string language;
            if (!ParseTemplatesAndCompileIntoSource(parameters, generator, parser, reporter, results, templateFiles, references, compilerInputs, out language))
                return results;

            // Get the CodeDOM Provider
            CodeDomProvider provider = CodeDomProvider.CreateProvider(language);

            // Generate Source Code
            List<string> templateCodeFiles = GetTemplateCodeFiles(parameters, reporter, results, provider);

            // Compile the code
            CompileCode(parameters, reporter, results, codeFiles, references, compilerInputs, provider, templateCodeFiles);

            return results;
        } 

        #endregion

        #region Private Methods

        private static string GetShortName(string path)
        {
            int lastSlash = path.LastIndexOf(Path.DirectorySeparatorChar);
            return path.Substring(lastSlash + 1);
        }

        private static void AddReferenceDirectives(ParsedTemplate template, List<ReferenceDirective> references)
        {
            foreach (IDirective directive in template.Directives)
            {
                // Changes from a test using 'is' and direct cast due to Microsoft Code Analysis Warning CA1800
                ReferenceDirective refDir = directive as ReferenceDirective;
                if (refDir != null)
                    references.Add(refDir);
            }
        }

        private static bool SplitFilesIntoTemplatesAndCodeFiles(string[] files, ProcessorParameters parameters, IProgressReporter reporter, ProcessorResults results, List<FileInfo> templateFiles, List<FileInfo> codeFiles)
        {
            using (reporter.StartTask(Resources.Task_AnalyzingInputFiles))
            {
                foreach (string file in files)
                {
                    if (!File.Exists(file))
                    {
                        results.GeneralProblems.Add(Problem.Error_InputFileDoesNotExist(file));
                        return false;
                    }
                    FileInfo fileInfo = new FileInfo(file);
                    if (parameters.TemplateFileExtensions.Contains(fileInfo.Extension))
                        templateFiles.Add(fileInfo);
                    else
                        codeFiles.Add(fileInfo);
                }
            }
            return true;
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1305:SpecifyIFormatProvider", MessageId = "System.String.Format(System.String,System.Object)")]
        private static bool ParseTemplatesAndCompileIntoSource(ProcessorParameters parameters, ITemplateGenerator generator, ITemplateParser parser, IProgressReporter reporter, ProcessorResults results, List<FileInfo> templateFiles, List<ReferenceDirective> references, List<string> compilerInputs, out string language)
        {
            language = String.Empty;
            int templateCount = 0;
            foreach (FileInfo file in templateFiles)
            {
                using (reporter.StartTask(String.Format(Resources.Task_GeneratingTemplateCode, file)))
                {
                    TemplateParserResults parserResults = null;
                    using (reporter.StartTask(Resources.Task_ParsingTemplateFile))
                    {
                        // Setup the processor results
                        results.IndividualResults.Add(file.Name, new IndividualProcessorResults());

                        // Parse the template
                        parserResults = parser.Parse(new StreamReader(file.FullName), reporter);
                        results.IndividualResults[file.Name].ParserResults = parserResults;
                        if (!parserResults.Success)
                        {
                            parserResults.Problems.Add(Problem.Error_UnableToContinueParsing());
                            continue;
                        }
                        if (parameters.CompileAssembly)
                            AddReferenceDirectives(parserResults.Output, references);

                        if (String.IsNullOrEmpty(language))
                        {
                            language = parserResults.Output.TemplateDirective.Language;
                        }
                        else if (!language.Equals(parserResults.Output.TemplateDirective.Language) && parameters.CompileAssembly)
                        {
                            results.GeneralProblems.Add(Problem.Error_LanguageMismatch());
                            return false;
                        }

                        if (!String.IsNullOrEmpty(parserResults.Output.TemplateDirective.CodeFile))
                            compilerInputs.Add(parserResults.Output.TemplateDirective.CodeFile);
                    }

                    TemplateGeneratorResults generatorResults = null;
                    using (reporter.StartTask(Resources.Task_GeneratingSourceCode))
                    {
                        // Generate Source for the template
                        generatorResults = generator.GenerateSource(parserResults.Output, file.Name, reporter);
                        results.IndividualResults[file.Name].GeneratorResults = generatorResults;
                        if (!generatorResults.Success)
                        {
                            generatorResults.Problems.Add(Problem.Error_UnableToContinueCompiling());
                            continue;
                        }
                        generatorResults.Output.UserData["TemplateFileName"] = file.Name;
                        templateCount++;
                    }
                }
            }
            if (templateCount == 0)
            {
                results.GeneralProblems.Add(Problem.Error_NoValidTemplatesToCompile());
                return false;
            }
            if (String.IsNullOrEmpty(language))
            {
                results.GeneralProblems.Add(Problem.Error_NoLanguageSpecified());
                return false;
            }
            return true;
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1305:SpecifyIFormatProvider", MessageId = "System.String.Format(System.String,System.Object,System.Object)"), System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands")]
        private static List<string> GetTemplateCodeFiles(ProcessorParameters parameters, IProgressReporter reporter, ProcessorResults results, CodeDomProvider provider)
        {
            List<string> templateCodeFiles = new List<string>();

            using (reporter.StartTask(Resources.Task_WritingTempCode))
            {
                CodeGeneratorOptions generatorOptions = new CodeGeneratorOptions();
                generatorOptions.IndentString = "\t";
                generatorOptions.VerbatimOrder = false;


                // Write code files for the templates
                foreach (KeyValuePair<string, IndividualProcessorResults> indResults in results.IndividualResults)
                {
                    CodeCompileUnit cu = indResults.Value.GeneratorResults.Output;
                    string codeFile = Path.Combine(parameters.CodeDirectory,
                        String.Format("{0}.{1}", cu.UserData["TemplateFileName"], provider.FileExtension));
                    if (File.Exists(codeFile)) File.Delete(codeFile);

                    StreamWriter sw = new StreamWriter(File.OpenWrite(codeFile));
                    provider.GenerateCodeFromCompileUnit(cu, sw, new CodeGeneratorOptions());
                    sw.Flush();
                    sw.Close();
                    templateCodeFiles.Add(codeFile);
                }
            }
            return templateCodeFiles;
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands")]
        private static void BuildReferencesList(ProcessorParameters parameters, IProgressReporter reporter, List<ReferenceDirective> references)
        {
            parameters.CompilerParameters.ReferencedAssemblies.AddRange(
                                    new string[] { "System.dll", "OpenArrow.Templating.dll", "OpenArrow.Utils.dll" });

            // Add references from the templates
            using (reporter.StartTask(Resources.Task_ScanningReferenceDirectives))
            {
                foreach (ReferenceDirective reference in references)
                {
                    string asmRef = reference.Assembly;
                    if (!asmRef.EndsWith(".dll"))
                        asmRef = String.Concat(asmRef, ".dll");
                    parameters.CompilerParameters.ReferencedAssemblies.Add(asmRef);
                }
            }
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands")]
        private static CompilerResults PerformCompilation(ProcessorParameters parameters, IProgressReporter reporter, List<string> compilerInputs, CodeDomProvider provider)
        {
            CompilerResults compilerResults;
            using (reporter.StartTask(Resources.Task_RunningCompiler))
            {
                compilerResults = provider.CompileAssemblyFromFile(parameters.CompilerParameters,
                    compilerInputs.ToArray());
            }
            return compilerResults;
        }

        private static void ConvertCompilerResults(IProgressReporter reporter, ProcessorResults results, CompilerResults compilerResults)
        {
            using (reporter.StartTask(Resources.Task_ConvertingResults))
            {
                foreach (KeyValuePair<string, IndividualProcessorResults> indResults in results.IndividualResults)
                {
                    indResults.Value.CompilerResults = new TemplateCompilerResults();
                    indResults.Value.CompilerResults.Success = !compilerResults.Errors.HasErrors;
                    indResults.Value.CompilerResults.CodeDomResults = compilerResults;
                }
                foreach (CompilerError error in compilerResults.Errors)
                {
                    Problem prob;
                    if (error.IsWarning)
                        prob = Problem.NewWarning(error.ErrorText, error.Line, error.Column, error.ErrorNumber);
                    else
                        prob = Problem.NewError(error.ErrorText, error.Line, error.Column, error.ErrorNumber);
                    string shortName = GetShortName(error.FileName);
                    if (String.IsNullOrEmpty(shortName) || !results.IndividualResults.ContainsKey(shortName))
                        results.GeneralProblems.Add(prob);
                    else
                        results.IndividualResults[shortName].CompilerResults.Problems.Add(prob);
                }
            }
        }

        private static void CompileCode(ProcessorParameters parameters, IProgressReporter reporter, ProcessorResults results, List<FileInfo> codeFiles, List<ReferenceDirective> references, List<string> compilerInputs, CodeDomProvider provider, List<string> templateCodeFiles)
        {
            if (parameters.CompileAssembly)
            {
                using (reporter.StartTask(Resources.Task_CompilingCode))
                {
                    // Get list of references from -References/-R arguments
                    BuildReferencesList(parameters, reporter, references);

                    compilerInputs.AddRange(templateCodeFiles);
                    foreach (FileInfo file in codeFiles)
                        compilerInputs.Add(file.FullName);

                    CompilerResults compilerResults = null;
                    compilerResults = PerformCompilation(parameters, reporter, compilerInputs, provider);

                    ConvertCompilerResults(reporter, results, compilerResults);
                }
            }
        } 

        #endregion
    }
}
